前言

Event Loop即事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。

先熟悉基本概念

【堆Heap】 堆是一种数据结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆是线性数据结构,相当于一维数组,有唯一后继。

【栈Stack】
栈在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。

栈是只能在某一端插入和删除的特殊线性表。

【队列Queue】
特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。

进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为空队列。

【进程】
进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。

【线程】
线程是进程的执行流,是CPU调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。

把这些概念拿到浏览器中来说,当你打开一个 Tab 页时,其实就是创建了一个进程,一个进程中可以有多个线程,比如渲染线程、JS 引擎线程、HTTP 请求线程等等。当你发起一个请求时,其实就是创建了一个线程,当请求结束后,该线程可能就会被销毁。

浏览器内核

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。

浏览器内核有多种线程在工作。

【GUI 渲染线程】
负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。

和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。

【JS 引擎线程】
单线程工作,负责解析运行 JavaScript 脚本。

和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。

【事件触发线程】
当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。

【定时器触发线程】
浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。

开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。

【http 请求线程】
http 请求的时候会开启一条请求线程。

请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。

基础知识我们基本了解了些必要的,下面我们开始介绍Event Loop

js中的任务分类

任务被分为两种,一种宏任务(MacroTask)也叫Task,一种叫微任务(MicroTask) 也叫jobs

【MacroTask(宏任务)】
类型:script全部代码、setTimeout、setInterval、setImmediate、I/O、UI Rendering

【MicroTask(微任务)】
类型:Process.nextTick(Node独有)、Promise 、MutationObserver

Event Loop

目前讨论的两种情况:浏览器的Event Loop 以及Node中的Event Loop

浏览器中的Event Loop

Javascript 有一个 main thread 主线程和 call-stack 调用栈(执行栈),所有的任务都会被放到调用栈等待主线程执行。

【JS调用栈】
JS调用栈采用的是后进先出的规则,当函数执行的时候,会被添加到栈的顶部,当执行栈执行完成后,就会从栈顶移出,直到栈内被清空。

【同步任务和异步任务】
Javascript单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行,异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。

浏览器进行事件循环工作方式

1、选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即null,则执行跳转到微任务(MicroTask)的执行步骤。

2、将事件循环中的任务设置为已选择任务。

3、执行任务。

4、将事件循环中当前运行任务设置为null。

5、将已经运行完成的任务从任务队列中删除。

6、microtasks步骤:进入microtask检查点。

7、更新界面渲染。

8、返回第一步。

【执行进入microtask检查点时,浏览器会执行以下步骤:】

设置microtask检查点标志为true。

当事件循环microtask执行不为空时:选择一个最先进入的microtask队列的microtask,将事件循环的microtask设置为已选择的microtask,运行microtask,将已经执行完成的microtask为null,移出microtask中的microtask。

清理IndexDB事务

设置进入microtask检查点的标志为false。

【重点】
总结以上规则为一条通俗好理解的:

1、顺序执行先执行同步方法,碰到MacroTask直接执行,并且把回调函数放入MacroTask执行队列中(下次事件循环执行);碰到microtask直接执行。把回调函数放入microtask执行队列中(本次事件循环执行)

2、当同步任务执行完毕后,去执行微任务microtask。(microtask队列清空)

3、由此进入下一轮事件循环:执行宏任务 MacroTask (setTimeout,setInterval,callback)

[总结]所有的异步都是为了按照一定的规则转换为同步方式执行。

查看一个示例

console.log('script start'); 

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

1、一开始task队列中只有script,则script中所有函数放入函数执行栈执行,代码按顺序执行。

2、接着遇到了setTimeout,它的作用是0ms后将回调函数放入task队列中,也就是说这个函数将在下一个事件循环中执行(注意这时候setTimeout执行完毕就返回了)。

3、接着遇到了Promise,按照前面所述Promise属于microtask,所以第一个.then()会放入microtask队列。

4、当所有script代码执行完毕后,此时函数执行栈为空。

5、开始检查microtask队列,此时队列不为空,执行.then()的回调函数输出'promise1',由于.then()返回的依然是promise,所以第二个.then()会放入microtask队列继续执行,输出'promise2'。此时microtask队列为空了

6、进入下一个事件循环,检查task队列发现了setTimeout的回调函数,立即执行回调函数输出'setTimeout',代码执行完毕。

小结

基本上能理解这个例子的话,对于浏览器的事件循环应该已经可以理解的差不多了。由于本篇文章涉及的知识点比较多,不易篇幅太长,至于node的事件循环方式则跟浏览器的实现方式不太一样。所以后面会在总结一篇文章

Copyright © frankshi.com 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-06-02 20:38:51

results matching ""

    No results matching ""